package ca.uvic.group2.server;

import ca.uvic.group2.ServerFactory;
import ca.uvic.group2.api.*;
import ca.uvic.group2.api.exceptions.*;
import java.io.Serializable;
import java.rmi.RemoteException;
import java.rmi.registry.*;
import java.rmi.server.UnicastRemoteObject;
import java.util.*;
import java.io.*;
import org.apache.log4j.*;
import java.net.*;

public class ServerRegistryImpl extends UnicastRemoteObject implements ServerRegistry,
                                                                       ServerManager,
                                                                       RemoteServer
{
    private class TransServer {
        public String hostName;
        public String serverId;
        public int numSessions;

        public TransServer(String hostName, String serverId) {
            this.hostName = hostName;
            this.serverId = serverId;
        }
    }

    private static final String INVALID_SERVER_ID = "";
    private static final String DB_SERVER_ID = "DBServer";
    private static final String QUOTE_SERVER_ID = "QuoteServer";
    private static final String AUDIT_LOG_ID = "AuditLog";
    private static final String TRANSACTION_SERVER_ID = "TransactionServer";

    private int _port;
    private String serverID = "ServerRegistry";
    private static Category debugLog = Logger.getInstance("RegistryDebug");

    private String _dbServerHost;
    private String _quoteServerHost;
    private String _auditLogHost;
    private String hostname;
    private Hashtable<String, TransServer> _transactionHosts = new Hashtable<String, TransServer>();
    private Hashtable<Long, TransServer> _sessions = new Hashtable<Long, TransServer>();
    private int _transactionIdSpool = 0;

    private ServerFactory factory = new ServerFactory();

    public ServerRegistryImpl() throws RemoteException {
        super();
        try {
            hostname = InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException err) {
            hostname = "Unknown";
        }
        debugLog.info("Server Registry started on host " + hostname);
    }

    //
    // RemoteServer
    //
    public void serverStart(int port) throws RemoteException, CommException {
        try {
            debugLog.info("RMI Startup on port " + Integer.toString(port));
            ServerFactory.serverStart(this, port, RMI_NAME);
            _port = port;
        } catch (Exception err) {
            err.printStackTrace();
            throw new CommException(err.getMessage());
        }
    }

    public void serverStop() throws RemoteException, CommException {
        try {
            debugLog.info("Shutdown signal received.");

            // Perform Shutdown
            factory.dispose();

            debugLog.info("Shutdown complete.");
        } catch (Exception err) {
            err.printStackTrace();
            throw new CommException(err.getMessage());
        }
    }

    public void setServerName(String name) throws RemoteException {
        serverID = name;
    }

    public String getServerName() throws RemoteException {
        return serverID;
    }

    public String getHostName() throws RemoteException {
        return hostname;
    }

    public void initSystem() throws RemoteException, CommException {
        try {
            Iterator<TransServer> i = _transactionHosts.values().iterator();
            while (i.hasNext()) {
                TransServer transServer = i.next();
                TransactionServer server = ServerFactory.getTransactionServerByHost(transServer.hostName);
                server.initSystem();
            }

            String dbHost = getDBServerHost();
            if (dbHost != null) {
                DBServer server = factory.getDBServer();
                server.initDatabase();
            }

            String auditHost = getAuditLogHost();
            if (auditHost != null) {
                AuditLog server = factory.getAuditServer();
                server.restartLog();
            }

            String quoteHost = getQuoteServerHost();
            if (quoteHost != null) {
                QuoteServer server = factory.getQuoteServer();
                server.initSystem();
            }
        } catch (IOException err) {
            debugLog.error("Init system failure", err);
            throw new CommException("Init system failure: " + err.getMessage());
        }
    }

    //
    // ServerRegistry
    //
    public String getTransactionServerHost(long sessionId) throws RemoteException {
        synchronized (_transactionHosts) {

            // 0 or 1 Transaction Server registered: No load balancing required.
            if (_transactionHosts.size() <= 1) {
                Iterator<TransServer> i = _transactionHosts.values().iterator();
                if (i.hasNext()) {
                    String hostName = i.next().hostName;
                    debugLog.debug("Returning transaction host " + hostName);
                    return hostName;
                }
                debugLog.warn("No Transaction server host available for session " + String.valueOf(sessionId));
                return null;
            }

            // More than one transaction server registered.  Load balancing...
            else { // (_transactionHosts.size() > 1) {

                if (sessionId == 0) {
                    // Find server with lowest number of open sessions
                    TransServer server = null;

                    Iterator<TransServer> i = _transactionHosts.values().iterator();
                    while (i.hasNext()) {
                        TransServer transServer = i.next();
                        if (server == null || server.numSessions > transServer.numSessions) {
                            server = transServer;
                        }
                    }

                    if (server != null) {
                        debugLog.debug("Returning transaction server with lowest load: " + server.hostName);
                        return server.hostName;
                    }
                    debugLog.fatal("Couldn't find a transaction server, but server set in non empty!");
                    return null;

                } else {

                    // Find server that owns the session
                    TransServer transServer;
                    synchronized (_sessions) {
                        transServer = _sessions.get(new Long(sessionId));
                    }
                    if (transServer == null) {
                        debugLog.warn("Couldn't find transaction server for session " + sessionId);
                        return null;
                    }
                    debugLog.debug("Return transaction server '" + transServer.hostName + "' for session " + sessionId);
                    return transServer.hostName;
                }
            }
        }
    }

    public String getQuoteServerHost() throws RemoteException {
        if (_quoteServerHost == null) {
            debugLog.warn("No Quote Server host available");
        }
        return _quoteServerHost;
    }

    public String getDBServerHost() throws RemoteException {
        if (_dbServerHost == null) {
            debugLog.warn("No DB Server host available");
        }
        return _dbServerHost;
    }

    public String getAuditLogHost() throws RemoteException {
        if (_auditLogHost == null) {
            debugLog.warn("No Audit Log Host available");
        }
        return _auditLogHost;
    }

    public void startStats() throws RemoteException {
        try {
            debugLog.debug("Start Stats");
            Iterator<TransServer> i = _transactionHosts.values().iterator();
            while (i.hasNext()) {
                TransServer transServer = i.next();
                TransactionServer server = ServerFactory.getTransactionServerByHost(transServer.hostName);
                ((StatsCollector)server).beginRun();
            }

            String dbHost = getDBServerHost();
            if (dbHost != null) {
                DBServer server = factory.getDBServer();
                if (server instanceof StatsCollector) {
                    ((StatsCollector)server).beginRun();
                }
            }

            String quoteHost = getQuoteServerHost();
            if (quoteHost != null) {
                QuoteServer server = factory.getQuoteServer();
                if (server instanceof StatsCollector) {
                    ((StatsCollector)server).beginRun();
                }
            }
        } catch (Exception err) {
            debugLog.error("startStats error." + err);
        }
    }

    public void stopStats() throws RemoteException {
        try {
            debugLog.debug("stop Stats");
            Iterator<TransServer> i = _transactionHosts.values().iterator();
            while (i.hasNext()) {
                TransServer transServer = i.next();
                TransactionServer server = ServerFactory.getTransactionServerByHost(transServer.hostName);
                ((StatsCollector)server).endRun();
            }

            String dbHost = getDBServerHost();
            if (dbHost != null) {
                DBServer server = factory.getDBServer();
                if (server instanceof StatsCollector) {
                    ((StatsCollector)server).endRun();
                }
            }

            String quoteHost = getQuoteServerHost();
            if (quoteHost != null) {
                QuoteServer server = factory.getQuoteServer();
                if (server instanceof StatsCollector) {
                    ((StatsCollector)server).endRun();
                }
            }
        } catch (Exception err) {
            debugLog.error("stopStats error." + err);
        }
    }

    public ArrayList getStats() throws RemoteException {
        debugLog.debug("Get Stats");
        ArrayList statsList = new ArrayList();

        try {
            Iterator<TransServer> i = _transactionHosts.values().iterator();
            while (i.hasNext()) {
                TransServer transServer = i.next();
                TransactionServer server = ServerFactory.getTransactionServerByHost(transServer.hostName);
                statsList.add(((StatsCollector)server).getStats());
            }

            String dbHost = getDBServerHost();
            if (dbHost != null) {
                DBServer server = factory.getDBServer();
                if (server instanceof StatsCollector) {
                    statsList.add(((StatsCollector)server).getStats());
                }
            }

            String quoteHost = getQuoteServerHost();
            if (quoteHost != null) {
                QuoteServer server = factory.getQuoteServer();
                if (server instanceof StatsCollector) {
                    statsList.add(((StatsCollector)server).getStats());
                }
            }
        } catch (Exception err) {
            debugLog.error("getStats error." + err);
        }
        return statsList;
    }

    //
    // ServerManager
    //
    public String registerServer(RemoteServer server, int kind) throws RemoteException, CommException {
        try {
            debugLog.debug("Registering server type " + String.valueOf(kind));
            String serverId = INVALID_SERVER_ID;
            String hostName;

            switch (kind) {
            case TRANSACTION_SERVER :
                synchronized (_transactionHosts) {
                    serverId = TRANSACTION_SERVER_ID + String.valueOf(++_transactionIdSpool);
                    hostName = server.getHostName();
                    debugLog.info("Register Transaction Server at " + hostName);
                    _transactionHosts.put(serverId, new TransServer(hostName, serverId));
                    server.setServerName(serverId);
                }
                break;
            case DB_SERVER :
                _dbServerHost = server.getHostName();
                debugLog.info("Register DB Server at " + _dbServerHost);
                serverId = DB_SERVER_ID;
                server.setServerName(serverId);
                break;
            case QUOTE_SERVER :
                _quoteServerHost = server.getHostName();
                debugLog.info("Register Quote server at " + _quoteServerHost);
                serverId = QUOTE_SERVER_ID;
                server.setServerName(serverId);
                break;
            case AUDIT_LOG :
                _auditLogHost = server.getHostName();
                debugLog.info("Register Log Server at " + _auditLogHost);
                serverId = AUDIT_LOG_ID;
                server.setServerName(serverId);
                break;
            default :
                throw new CommException("Invalid server kind.");
            }
            debugLog.info("Server Registered: " + serverId);
            return serverId;
        } catch (RuntimeException err) {
            debugLog.error("Server registration runtime error.", err);
            throw err;
        } catch (CommException err) {
            debugLog.error("Server registration comm error.", err);
            throw err;
        }
    }

    public void unregisterServer(RemoteServer server) throws RemoteException, CommException {
        try {
            debugLog.debug("Unregister server");
            String id = server.getServerName();
            if (id == null || id.length() == 0) {
                throw new CommException("Invalid server id");
            }

            String hostname = server.getHostName();
            if (hostname == null || hostname.length() == 0) {
                throw new CommException("Server has null host name");
            }

            debugLog.info("Unregistering " + id + " at " + hostname);

            if (DB_SERVER_ID.equals(id)) {
                if (hostname.equals(_dbServerHost)) {
                    debugLog.info("DB Server unregistered");
                    _dbServerHost = null;
                }
            } else if (QUOTE_SERVER_ID.equals(id)) {
                if (hostname.equals(_quoteServerHost)) {
                    debugLog.info("Quote Server unregistered");
                    _quoteServerHost = null;
                }
            } else if (AUDIT_LOG_ID.equals(id)) {
                if (hostname.equals(_auditLogHost)) {
                    debugLog.info("Audit Log Server unregistered");
                    _auditLogHost = null;
                }
            } else if (id.startsWith(TRANSACTION_SERVER_ID)) {
                synchronized (_transactionHosts) {
                    if (_transactionHosts.containsKey(id)) {
                        debugLog.info("Transaction Server unregistered");
                        _transactionHosts.remove(id);

                        // Remove any open sessions from session list
                        synchronized (_sessions) {
                            Iterator<TransServer> i = _sessions.values().iterator();
                            while (i.hasNext()) {
                                if (i.next().serverId.equals(id)) {
                                    i.remove();
                                }
                            }
                        }
                    } else {
                        throw new CommException("Cannot unregister server: no matching transaction server");
                    }
                }
            }
        } catch (RuntimeException err) {
            debugLog.error("Server de-registration runtime error.", err);
            throw err;
        } catch (CommException err) {
            debugLog.error("Server de-registration comm error.", err);
            throw err;
        }
    }


    public void unregisterAllServers() throws RemoteException {
        debugLog.info("Unregistering all servers");
        _dbServerHost = null;
        _quoteServerHost = null;
        _auditLogHost = null;
        synchronized (_transactionHosts) {
            _transactionHosts.clear();
        }
        synchronized (_sessions) {
            _sessions.clear();
        }
    }

    private TransServer getTransactionHostName(String serverId) throws SessionNotFoundException {
        synchronized (_transactionHosts) {
            TransServer server = this._transactionHosts.get(serverId);
            if (server == null) {
                throw new SessionNotFoundException("Transaction Server ID not found: " + serverId);
            }
            return server;
        }
    }

    public void registerSession(long sessionId, String serverId) throws RemoteException, SessionNotFoundException {
        try {
            if (debugLog.isDebugEnabled()) {
                debugLog.debug("Registering session " + sessionId + " with server " + serverId);
            }

            TransServer server = getTransactionHostName(serverId); // verify that the server ID exists
            server.numSessions++;
            synchronized (_sessions) {
                _sessions.put(new Long(sessionId), server);
            }

        } catch (SessionNotFoundException err) {
            debugLog.error("registerSession: server id not found", err);
            throw err;
        } catch (RuntimeException err) {
            debugLog.error("registerSession: RuntimeException", err);
            throw err;
        }
    }

    public void unregisterSession(long sessionId, String serverId) throws RemoteException, SessionNotFoundException {
        try {
            if (debugLog.isDebugEnabled()) {
                debugLog.debug("Unregistering session " + sessionId + " from server " + serverId);
            }

            TransServer server = getTransactionHostName(serverId); // verify that the server ID exists
            server.numSessions--;
            synchronized (_sessions) {
                TransServer removed = _sessions.remove(new Long(sessionId));
                if (removed != server) {
                    debugLog.fatal("unregisterSession: mismatched server IDs!");
                }
            }

        } catch (SessionNotFoundException err) {
            debugLog.error("registerSession: server id not found", err);
            throw err;
        } catch (RuntimeException err) {
            debugLog.error("registerSession: RuntimeException", err);
            throw err;
        }


    }

}
